#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# PYTHON_ARGCOMPLETE_OK
# File name          : __main__.py
# Author             : Podalirius (@podalirius_)
# Date created       : 17 Sep 2022


import argparse
import os
import sys
import threading

import argcomplete
from sectools.network.domains import is_fqdn
from sectools.network.ip import (expand_cidr, is_ipv4_addr, is_ipv4_cidr,
                                 is_ipv6_addr)

VERSION = "2.4.3"

banner = (
    """       ______
      / ____/___  ___  _____________  _____
     / /   / __ \\/ _ \\/ ___/ ___/ _ \\/ ___/
    / /___/ /_/ /  __/ /  / /__/  __/ /      v%s
    \\____/\\____/\\___/_/   \\___/\\___/_/       by Remi GASCOU (Podalirius)
"""
    % VERSION
)


def parseArgs():
    print(banner)
    parser = argparse.ArgumentParser(
        add_help=True,
        description="Automatic windows authentication coercer using various methods.",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        default=False,
        action="store_true",
        help="Verbose mode (default: False)",
    )
    parser.add_argument(
        "--debug",
        default=False,
        action="store_true",
        help="Debug mode (default: False)",
    )
    parser.add_argument(
        "--disable-escape-codes",
        default=False,
        action="store_true",
        help="Disable ANSI escape codes",
    )

    # Creating the "scan" subparser ==============================================================================================================
    mode_scan = argparse.ArgumentParser(add_help=False)
    mode_scan.add_argument(
        "-v",
        "--verbose",
        default=False,
        action="store_true",
        help="Verbose mode (default: False)",
    )
    mode_scan.add_argument(
        "--debug",
        default=False,
        action="store_true",
        help="Debug mode (default: False)",
    )
    mode_scan.add_argument(
        "--disable-escape-codes",
        default=False,
        action="store_true",
        help="Disable ANSI escape codes",
    )
    # Advanced configuration
    mode_scan_advanced_config = mode_scan.add_argument_group("Advanced options")
    mode_scan_advanced_config.add_argument(
        "--export-json",
        default=None,
        type=str,
        help="Export results to specified JSON file.",
    )
    mode_scan_advanced_config.add_argument(
        "--export-xlsx",
        default=None,
        type=str,
        help="Export results to specified XLSX file.",
    )
    mode_scan_advanced_config.add_argument(
        "--export-sqlite",
        default=None,
        type=str,
        help="Export results to specified SQLITE3 database file.",
    )
    mode_scan_advanced_config.add_argument(
        "--delay", default=None, type=int, help="Delay between attempts (in seconds)"
    )
    mode_scan_advanced_config.add_argument(
        "--min-http-port",
        default=64000,
        type=int,
        help="Minimum HTTP port for listening (default: 64000)",
    )
    mode_scan_advanced_config.add_argument(
        "--max-http-port",
        default=65000,
        type=int,
        help="Maximum HTTP port for listening (default: 65000)",
    )
    mode_scan_advanced_config.add_argument(
        "--http-port", default=80, type=int, help="HTTP port (default: 80)"
    )
    mode_scan_advanced_config.add_argument(
        "--smb-port", default=445, type=int, help="SMB port (default: 445)"
    )
    mode_scan_advanced_config.add_argument(
        "--dce-port", default=135, type=int, help="DCERPC port (default: 135)"
    )
    mode_scan_advanced_config.add_argument(
        "--dce-ports", default=[], nargs="+", type=int, help="DCERPC ports"
    )
    mode_scan_advanced_config.add_argument(
        "--auth-type",
        default=None,
        type=str,
        choices=("smb", "http"),
        help="Desired authentication type.",
    )
    mode_scan_advanced_config.add_argument(
        "--stop-on-ntlm-auth",
        default=False,
        action="store_true",
        help="Move on to next target on successful NTLM authentication.",
    )
    # Filters
    mode_scan_filters = mode_scan.add_argument_group("Filtering")
    mode_scan_filters.add_argument(
        "--filter-method-name",
        default=[],
        action="append",
        type=str,
        help="Filter by method name",
    )
    mode_scan_filters.add_argument(
        "--filter-protocol-name",
        default=[],
        action="append",
        type=str,
        help="Filter by protocol name",
    )
    mode_scan_filters.add_argument(
        "--filter-pipe-name",
        default=[],
        action="append",
        type=str,
        help="Filter by pipe name",
    )
    mode_scan_filters.add_argument(
        "--filter-transport-name",
        default=["msrpc", "dcerpc"],
        choices=["msrpc", "dcerpc"],
        nargs="*",
        type=str,
        help="Filter by transport name",
    )
    # Credentials
    mode_scan_credentials = mode_scan.add_argument_group("Credentials")
    mode_scan_credentials.add_argument(
        "-u",
        "--username",
        default="",
        help="Username to authenticate to the remote machine.",
    )
    mode_scan_credentials.add_argument(
        "-p",
        "--password",
        default="",
        help="Password to authenticate to the remote machine. (if omitted, it will be asked unless -no-pass is specified)",
    )
    mode_scan_credentials.add_argument(
        "-d",
        "--domain",
        default="",
        help="Windows domain name to authenticate to the machine.",
    )
    mode_scan_credentials.add_argument(
        "--hashes",
        action="store",
        metavar="[LMHASH]:NTHASH",
        help="NT/LM hashes (LM hash can be empty)",
    )
    mode_scan_credentials.add_argument(
        "--no-pass", action="store_true", help="Don't ask for password (useful for -k)"
    )
    mode_scan_credentials.add_argument(
        "--dc-ip",
        action="store",
        metavar="ip address",
        help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter",
    )
    # Targets source
    mode_scan_targets_source = mode_scan.add_mutually_exclusive_group(required=True)
    mode_scan_targets_source.add_argument(
        "-t",
        "--target-ip",
        default=None,
        help="IP address or hostname of the target machine",
    )
    mode_scan_targets_source.add_argument(
        "-f",
        "--targets-file",
        default=None,
        help="File containing a list of IP address or hostname of the target machines",
    )
    # Listener
    mode_scan_targets_listener = mode_scan.add_mutually_exclusive_group(required=False)
    mode_scan_targets_listener.add_argument(
        "-i",
        "--interface",
        default=None,
        help="Interface to listen on incoming authentications.",
    )
    mode_scan_targets_listener.add_argument(
        "-I",
        "--ip-address",
        default=None,
        help="IP address to listen on incoming authentications.",
    )
    mode_scan_targets_listener.add_argument(
        "--path-ip",
        default=None,
        help="IP address to use when generating exploit paths.",
    )
    # Logging
    mode_scan_logging = mode_scan.add_argument_group("Logging")
    mode_scan_logging.add_argument(
        "--minimum-log-level", default=0, help="Minimum logging level (integer)."
    )
    mode_scan_logging.add_argument(
        "--log-file",
        default=None,
        help="Path for the file to log to (enables logging).",
    )

    # Creating the "fuzz" subparser ==============================================================================================================
    mode_fuzz = argparse.ArgumentParser(add_help=False)
    mode_fuzz.add_argument(
        "-v",
        "--verbose",
        default=False,
        action="store_true",
        help="Verbose mode (default: False)",
    )
    mode_fuzz.add_argument(
        "--debug",
        default=False,
        action="store_true",
        help="Debug mode (default: False)",
    )
    mode_fuzz.add_argument(
        "--disable-escape-codes",
        default=False,
        action="store_true",
        help="Disable ANSI escape codes",
    )
    # Advanced configuration
    mode_fuzz_advanced_config = mode_fuzz.add_argument_group("Advanced configuration")
    mode_fuzz_advanced_config.add_argument(
        "--export-json",
        default=None,
        type=str,
        help="Export results to specified JSON file.",
    )
    mode_fuzz_advanced_config.add_argument(
        "--export-xlsx",
        default=None,
        type=str,
        help="Export results to specified XLSX file.",
    )
    mode_fuzz_advanced_config.add_argument(
        "--export-sqlite",
        default=None,
        type=str,
        help="Export results to specified SQLITE3 database file.",
    )
    mode_fuzz_advanced_config.add_argument(
        "--delay", default=None, type=int, help="Delay between attempts (in seconds)"
    )
    mode_fuzz_advanced_config.add_argument(
        "--min-http-port",
        default=64000,
        type=int,
        help="Minimum HTTP port for listening (default: 64000)",
    )
    mode_fuzz_advanced_config.add_argument(
        "--max-http-port",
        default=65000,
        type=int,
        help="Maximum HTTP port for listening (default: 65000)",
    )
    mode_fuzz_advanced_config.add_argument(
        "--http-port", default=80, type=int, help="HTTP port (default: 80)"
    )
    mode_fuzz_advanced_config.add_argument(
        "--smb-port", default=445, type=int, help="SMB port (default: 445)"
    )
    mode_fuzz_advanced_config.add_argument(
        "--dce-port", default=135, type=int, help="DCERPC port (default: 135)"
    )
    mode_fuzz_advanced_config.add_argument(
        "--dce-ports", default=[], nargs="+", type=int, help="DCERPC ports"
    )
    mode_fuzz_advanced_config.add_argument(
        "--auth-type",
        default=None,
        type=str,
        choices=("smb", "http"),
        help="Desired authentication type.",
    )
    # Filters
    mode_fuzz_filters = mode_fuzz.add_argument_group("Filtering")
    mode_fuzz_filters.add_argument(
        "--filter-method-name",
        default=[],
        action="append",
        type=str,
        help="Filter by method name",
    )
    mode_fuzz_filters.add_argument(
        "--filter-protocol-name",
        default=[],
        action="append",
        type=str,
        help="Filter by protocol name",
    )
    mode_fuzz_filters.add_argument(
        "--filter-pipe-name",
        default=[],
        action="append",
        type=str,
        help="Filter by pipe name",
    )
    mode_fuzz_filters.add_argument(
        "--filter-transport-name",
        default=["msrpc", "dcerpc"],
        choices=["msrpc", "dcerpc"],
        nargs="*",
        type=str,
        help="Filter by transport name",
    )
    # Credentials
    mode_fuzz_credentials = mode_fuzz.add_argument_group("Credentials")
    mode_fuzz_credentials.add_argument(
        "--only-known-exploit-paths",
        action="store_true",
        default=False,
        help="Only test known exploit paths for each functions",
    )
    mode_fuzz_credentials.add_argument(
        "-u",
        "--username",
        default="",
        help="Username to authenticate to the remote machine.",
    )
    mode_fuzz_credentials.add_argument(
        "-p",
        "--password",
        default="",
        help="Password to authenticate to the remote machine. (if omitted, it will be asked unless -no-pass is specified)",
    )
    mode_fuzz_credentials.add_argument(
        "-d",
        "--domain",
        default="",
        help="Windows domain name to authenticate to the machine.",
    )
    mode_fuzz_credentials.add_argument(
        "--hashes",
        action="store",
        metavar="[LMHASH]:NTHASH",
        help="NT/LM hashes (LM hash can be empty)",
    )
    mode_fuzz_credentials.add_argument(
        "--no-pass", action="store_true", help="Don't ask for password (useful for -k)"
    )
    mode_fuzz_credentials.add_argument(
        "--dc-ip",
        action="store",
        metavar="ip address",
        help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter",
    )
    # Targets source
    mode_fuzz_targets_source = mode_fuzz.add_mutually_exclusive_group(required=True)
    mode_fuzz_targets_source.add_argument(
        "-t",
        "--target-ip",
        default=None,
        help="IP address or hostname of the target machine",
    )
    mode_fuzz_targets_source.add_argument(
        "-f",
        "--targets-file",
        default=None,
        help="File containing a list of IP address or hostname of the target machines",
    )
    # Listener
    mode_fuzz_targets_listener = mode_fuzz.add_mutually_exclusive_group(required=False)
    mode_fuzz_targets_listener.add_argument(
        "-i",
        "--interface",
        default=None,
        help="Interface to listen on incoming authentications.",
    )
    mode_fuzz_targets_listener.add_argument(
        "-I",
        "--ip-address",
        default=None,
        help="IP address to listen on incoming authentications.",
    )
    mode_fuzz_targets_listener.add_argument(
        "--path-ip",
        default=None,
        help="IP address to use when generating exploit paths.",
    )
    # Logging
    mode_fuzz_logging = mode_fuzz.add_argument_group("Logging")
    mode_fuzz_logging.add_argument(
        "--minimum-log-level", default=0, help="Minimum logging level (integer)."
    )
    mode_fuzz_logging.add_argument(
        "--log-file",
        default=None,
        help="Path for the file to log to (enables logging).",
    )

    # Creating the "coerce" subparser ==============================================================================================================
    mode_coerce = argparse.ArgumentParser(add_help=False)
    mode_coerce.add_argument(
        "-v",
        "--verbose",
        default=False,
        action="store_true",
        help="Verbose mode (default: False)",
    )
    mode_coerce.add_argument(
        "--debug",
        default=False,
        action="store_true",
        help="Debug mode (default: False)",
    )
    mode_coerce.add_argument(
        "--disable-escape-codes",
        default=False,
        action="store_true",
        help="Disable ANSI escape codes",
    )
    # Advanced configuration
    mode_coerce_advanced_config = mode_coerce.add_argument_group(
        "Advanced configuration"
    )
    mode_coerce_advanced_config.add_argument(
        "--delay", default=None, type=int, help="Delay between attempts (in seconds)"
    )
    mode_coerce_advanced_config.add_argument(
        "--http-port", default=80, type=int, help="HTTP port (default: 80)"
    )
    mode_coerce_advanced_config.add_argument(
        "--smb-port", default=445, type=int, help="SMB port (default: 445)"
    )
    mode_coerce_advanced_config.add_argument(
        "--dce-port", default=135, type=int, help="DCERPC port (default: 135)"
    )
    mode_coerce_advanced_config.add_argument(
        "--dce-ports", default=[], nargs="+", type=int, help="DCERPC ports"
    )
    mode_coerce_advanced_config.add_argument(
        "--always-continue",
        default=False,
        action="store_true",
        help="Always continue to coerce",
    )
    mode_coerce_advanced_config.add_argument(
        "--auth-type",
        default=None,
        type=str,
        choices=("smb", "http"),
        help="Desired authentication type.",
    )
    # Filters
    mode_coerce_filters = mode_coerce.add_argument_group("Filtering")
    mode_coerce_filters.add_argument(
        "--filter-method-name",
        default=[],
        action="append",
        type=str,
        help="Filter by method name",
    )
    mode_coerce_filters.add_argument(
        "--filter-protocol-name",
        default=[],
        action="append",
        type=str,
        help="Filter by protocol name",
    )
    mode_coerce_filters.add_argument(
        "--filter-pipe-name",
        default=[],
        action="append",
        type=str,
        help="Filter by pipe name",
    )
    mode_coerce_filters.add_argument(
        "--filter-transport-name",
        default=["msrpc", "dcerpc"],
        choices=["msrpc", "dcerpc"],
        nargs="*",
        type=str,
        help="Filter by transport name",
    )
    # Credentials
    mode_coerce_credentials = mode_coerce.add_argument_group("Credentials")
    mode_coerce_credentials.add_argument(
        "-u", "--username", default="", help="Username to authenticate to the machine."
    )
    mode_coerce_credentials.add_argument(
        "-p",
        "--password",
        default="",
        help="Password to authenticate to the machine. (if omitted, it will be asked unless -no-pass is specified)",
    )
    mode_coerce_credentials.add_argument(
        "-d",
        "--domain",
        default="",
        help="Windows domain name to authenticate to the machine.",
    )
    mode_coerce_credentials.add_argument(
        "--hashes",
        action="store",
        metavar="[LMHASH]:NTHASH",
        help="NT/LM hashes (LM hash can be empty)",
    )
    mode_coerce_credentials.add_argument(
        "--no-pass", action="store_true", help="Don't ask for password (useful for -k)"
    )
    mode_coerce_credentials.add_argument(
        "--dc-ip",
        action="store",
        metavar="ip address",
        help="IP Address of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter",
    )
    # Targets source
    mode_coerce_targets_source = mode_coerce.add_mutually_exclusive_group(required=True)
    mode_coerce_targets_source.add_argument(
        "-t",
        "--target-ip",
        default=None,
        help="IP address or hostname of the target machine",
    )
    mode_coerce_targets_source.add_argument(
        "-f",
        "--targets-file",
        default=None,
        help="File containing a list of IP address or hostname of the target machines",
    )
    # Listener
    listener_group = mode_coerce.add_argument_group("Listener")
    listener_group.add_argument(
        "-l",
        "--listener-ip",
        required=True,
        type=str,
        help="IP address or hostname of the listener machine",
    )
    listener_group.add_argument(
        "-i",
        "--interface",
        default=None,
        help="Interface to listen on incoming authentications.",
    )
    listener_group.add_argument(
        "-I",
        "--ip-address",
        default=None,
        help="IP address to listen on incoming authentications.",
    )
    listener_group.add_argument(
        "--path-ip",
        default=None,
        help="IP address to use when generating exploit paths.",
    )
    # Logging
    mode_coerce_logging = mode_coerce.add_argument_group("Logging")
    mode_coerce_logging.add_argument(
        "--minimum-log-level", default=0, help="Minimum logging level (integer)."
    )
    mode_coerce_logging.add_argument(
        "--log-file",
        default=None,
        help="Path for the file to log to (enables logging).",
    )

    # Adding the subparsers to the base parser
    subparsers = parser.add_subparsers(help="Mode", dest="mode", required=True)
    subparsers.add_parser(
        "scan",
        parents=[mode_scan],
        help="Tests known methods with known working paths on all methods, and report when an authentication is received.",
    )
    subparsers.add_parser(
        "coerce",
        parents=[mode_coerce],
        help="Trigger authentications through all known methods with known working paths.",
    )
    subparsers.add_parser(
        "fuzz",
        parents=[mode_fuzz],
        help="Tests every method with a list of exploit paths, and report when an authentication is received.",
    )

    argcomplete.autocomplete(parser, always_complete_options=False)
    options = parser.parse_args()

    # Parsing hashes
    lmhash, nthash = "", ""
    if options.hashes is not None:
        lmhash, nthash = options.hashes.split(":")
    if (
        options.password == ""
        and options.username != ""
        and options.hashes is None
        and options.no_pass is not True
    ):
        from getpass import getpass

        options.password = getpass("Password:")

    return lmhash, nthash, options


def main():
    lmhash, nthash, options = parseArgs()

    from coercer.core.Reporter import create_reporter

    create_reporter(options, options.verbose)

    from coercer.core.loader import find_and_load_coerce_methods
    from coercer.core.modes.coerce import action_coerce
    from coercer.core.modes.fuzz import action_fuzz
    from coercer.core.modes.scan import action_scan
    from coercer.core.Reporter import reporter
    from coercer.network.smb import try_login
    from coercer.network.utils import can_listen_on_port, redirect_smb_packets
    from coercer.structures.Credentials import Credentials

    available_methods = find_and_load_coerce_methods()

    if (
        options.smb_port == 445
        and sys.platform == "win32"
        and (options.mode == "scan" or options.mode == "fuzz")
    ):
        reporter.print_info("Redirecting packets between port 445 <-> 4445")
        options.redirecting_smb_packets = True
        redirector = threading.Thread(target=redirect_smb_packets)
        redirector.start()
    else:
        options.redirecting_smb_packets = False

    # Parsing targets
    targets = []
    if options.target_ip is not None:
        targets = [options.target_ip]
    elif options.targets_file is not None:
        if os.path.exists(options.targets_file):
            f = open(options.targets_file, "r")
            targets = sorted(list(set([line.strip() for line in f.readlines()])))
            f.close()
            reporter.print_info("Loaded %d targets." % len(targets))
        else:
            reporter.print_error(
                "Could not open targets file '%s'." % options.targets_file
            )
            sys.exit(0)

    # Sort uniq on targets list
    targets = [t.strip() for t in targets if len(t.strip()) != 0]
    targets = sorted(list(set(targets)))

    final_targets = []
    # Parsing target to filter IP/DNS/CIDR
    for target in targets:
        if is_ipv4_cidr(target):
            final_targets += [ip for ip in expand_cidr(target)]
        elif is_ipv4_addr(target):
            final_targets.append(target)
        elif is_ipv6_addr(target):
            final_targets.append(target)
        elif is_fqdn(target):
            final_targets.append(target)
        elif target.startswith("http://") or target.startswith("https://"):
            import urllib.parse

            target = urllib.parse.urlparse(target).netloc
            final_targets.append(target)
        else:
            reporter.print_warn("Target '%s' was not added." % target, debug=True)

    # Sort
    targets = sorted(list(set(final_targets)))

    credentials = Credentials(
        username=options.username,
        password=options.password,
        domain=options.domain,
        lmhash=lmhash,
        nthash=nthash,
    )

    # Processing actions
    if options.mode == "coerce":
        reporter.print_info("Starting coerce mode")
        if credentials.is_anonymous():
            reporter.print_info(
                "No credentials provided, trying to connect with a NULL session."
            )
        for target in targets:
            reporter.print_info("Scanning target %s" % target)
            # Checking credentials if any
            if "msrpc" not in options.filter_transport_name or try_login(
                credentials, target
            ):
                try:
                    # Starting action
                    action_coerce(target, available_methods, options, credentials)
                except Exception as e:
                    reporter.print_warn("An unexpected error occurred: %s" % e)
    elif options.mode == "scan":
        reporter.print_info("Starting scan mode")
        if credentials.is_anonymous():
            reporter.print_info(
                "No credentials provided, trying to connect with a NULL session."
            )
        if not can_listen_on_port(
            "0.0.0.0", 4445 if options.redirecting_smb_packets else options.smb_port
        ):
            reporter.print_error(
                "Cannot listen on port tcp/%d. Are you root or are other servers running?"
                % 4445
                if options.redirecting_smb_packets
                else options.smb_port
            )
        else:
            for target in targets:
                reporter.print_info("Scanning target %s" % target)
                # Checking credentials if any
                if "msrpc" not in options.filter_transport_name or try_login(
                    credentials, target
                ):
                    try:
                        # Starting action
                        action_scan(target, available_methods, options, credentials)
                    except Exception as e:
                        reporter.print_warn("An unexpected error occurred: %s" % e)
            # Reporting results
            if options.export_json is not None:
                reporter.exportJSON(options.export_json)
            if options.export_xlsx is not None:
                reporter.exportXLSX(options.export_xlsx)
            if options.export_sqlite is not None:
                reporter.exportSQLITE(options.export_sqlite)

    elif options.mode == "fuzz":
        reporter.print_info("Starting fuzz mode")
        if credentials.is_anonymous():
            reporter.print_info(
                "No credentials provided, trying to connect with a NULL session."
            )
        if not can_listen_on_port(
            "0.0.0.0", 4445 if options.redirecting_smb_packets else options.smb_port
        ):
            reporter.print_error(
                "Cannot listen on port tcp/%d. Are you root or are other servers running?"
                % 4445
                if options.redirecting_smb_packets
                else options.smb_port
            )
        else:
            for target in targets:
                reporter.print_info("Fuzzing target %s" % target)
                # Checking credentials if any
                if "msrpc" not in options.filter_transport_name or try_login(
                    credentials, target
                ):
                    try:
                        # Starting action
                        action_fuzz(target, available_methods, options, credentials)
                    except Exception as e:
                        reporter.print_warn("An unexpected error occurred: %s" % e)
            # Reporting results
            if options.export_json is not None:
                reporter.exportJSON(options.export_json)
            if options.export_xlsx is not None:
                reporter.exportXLSX(options.export_xlsx)
            if options.export_sqlite is not None:
                reporter.exportSQLITE(options.export_sqlite)

    reporter.print_ok("All done! Bye Bye!")


if __name__ == "__main__":
    main()
